import { Script } from 'vm';
import { transform } from 'babel-core';
import Promise from 'bluebird';
import lodash from 'lodash';
import projectDir, { read } from 'fs-jetpack';
import stripeJSONComments from 'strip-json-comments';
import Geetest from 'geetest';
import request from 'request-promise';
import crypto from 'crypto';

import env from '../../../../conf.js';

// Hold all module already loaded
const modules = new Map();
const envCloned = Object.assign({}, env);

/**
 * Parse string with comments to json object
 * @param  {string} str
 * @return {Object}
 */
export function parseJSON(str) {
    try {
        return JSON.parse(stripeJSONComments(str));
    } catch (e) {
        console.error('failed to parse:', e.message);
        return {};
    }
}

/**
 * Reset module caches
 */
export function reset() {
    modules.clear();
    modules.set('lodash', lodash);
    modules.set('geetest', Geetest);
    modules.set('request', request);
    modules.set('env', envCloned);
    modules.set('crypto', crypto);
    modules.set('utils', {
        parseJSON
    });
}

/**
 * Create global context for the sandbox
 * @return {Object}
 */
function createGlobalContext() {
    return {
        // Use bluebird as Promise implementation
        Promise,

        define: defineModule
    }
}

/**
 * Review and transpile ES6 codes
 * @param  {string}     source
 * @param  {string}     options.filename
 * @param  {string}     options.moduleId
 * @return {Script}
 */
export function review(source, { filename = 'vm.js', moduleId } = {}) {
    let {
        code,
        map
    } = transform(source, {
        presets: [
            'es2015-node6',
            'stage-1'
        ],
        plugins: [
            'transform-es2015-modules-amd'
        ],
        moduleId
    });

    // Compile the "transpiled/babelified" code and return with a
    // filename (default: 'vm.js')
    return new Script(code, {
        // allows you to control the filename that shows up in any stack traces
        // produced from this script.
        filename
    });
}

/**
 * Execute ES6 codes in new context
 * @param  {string}     source
 * @param  {string}     options.filename
 * @param  {string}     options.moduleId
 * @return {[type]}                  [description]
 */
export function execDefine(code, { filename, moduleId } = {}) {
    // Review the code before actually executing it
    const script = review(code, { filename, moduleId });
    let result;
    let sandbox = createGlobalContext();

    try {
        result = script.runInNewContext(sandbox);
    } catch (e) {
        console.log('failed executing', {
            message: e.message,
            stack: e.stack
        });
    }

    // Return the result
    return result;
}

/**
 * Reslve module from js file based on module name
 * @param  {string} moduleId
 * @return {Object}
 */
function resolveFromFile(moduleId) {
    const controllerPath = projectDir.path(`./raml/controllers/${moduleId}.js`);
    const source = read(controllerPath);

    if (source) {
        return execDefine(source, {
            filename: controllerPath,
            moduleId
        });
    }

    throw new Error(`Cannot resolve module "${moduleId}"`);
}

/**
 * Resolve a module
 * @param  {string} moduleId
 * @return {Promise}
 */
export function resolve(moduleId) {
    if (modules.has(moduleId)) {
        return modules.get(moduleId);
    }

    return resolveFromFile(moduleId);
}

/**
 * Load dependencies and run
 * @param  {string[]} dependencies
 * @param  {Function} injector
 * @return {Promise}
 */
export function requireModule(dependencies, injector) {
    let exports = {};

    const moduleRequired = dependencies.map(moduleId => {
        return moduleId === 'exports' ?
            exports : resolve(moduleId);
    });

    injector.apply(null, moduleRequired);

    return exports;
}

/**
 * Define a module
 * @param  {string} moduleId
 * @param  {string[]} dependencies
 * @param  {Function} injector
 * @return {Promise}
 */
export function defineModule(moduleId, dependencies, injector) {
    const exports = requireModule(dependencies, injector);
    modules.set(moduleId, exports);
    return exports;
}
